#include "xeglview.h"
#include "GL/glew.h"
#include <string>
#include <stdarg.h>

void xMessageBox(const char *msg, const char *title)
{
    MessageBoxA(NULL, msg, title, MB_OK);
}

#define MAX_LEN 1024

void xLog(const char *format, ...)
{
    char buf[MAX_LEN];

    va_list ap;
    va_start(ap, format);
    vsnprintf_s(buf, MAX_LEN, MAX_LEN, format, ap);
    va_end(ap);

    WCHAR wbuf[MAX_LEN] = {0};
    MultiByteToWideChar(CP_UTF8, 0, buf, -1, wbuf, sizeof(wbuf));
    OutputDebugStringW(wbuf);
    OutputDebugStringA("\n");

    WideCharToMultiByte(CP_ACP, 0, wbuf, sizeof(wbuf), buf, sizeof(buf), NULL, FALSE);
    printf("%s\n", buf);
}

static void setupPixelFormat(HDC hdc)
{
    int pixelFormat;

    PIXELFORMATDESCRIPTOR pfd = 
    {
        sizeof(PIXELFORMATDESCRIPTOR),      // Size
        1,                                  // version
        PFD_SUPPORT_OPENGL |                // OpenGL window
        PFD_DRAW_TO_WINDOW |                // Render to window
        PFD_DOUBLEBUFFER,                   // Support double-buffering
        PFD_TYPE_RGBA,                      // Color type
        32,                                 // Preferred color depth
        0, 0, 0, 0, 0, 0,                   // Color bits (ignored)
        0,                                  // No alpha buffer
        0,                                  // Alpha bits (ignored)
        0,                                  // No accumulation buffer
        0, 0, 0, 0,                         // Accum bits (ignored)
        24,                                 // Depth buffer
        8,                                  // No stencil buffer
        0,                                  // No auxiliary buffers
        PFD_MAIN_PLANE,                     // Main layer
        0,                                  // Reserved
        0, 0, 0,                            // No layer, visible, damage masks
    };

    pixelFormat = ::ChoosePixelFormat(hdc, &pfd);
    ::SetPixelFormat(hdc, pixelFormat, &pfd);
}

static bool glewDynamicBinding()
{
    const char *glExtensions = (const char *)glGetString(GL_EXTENSIONS);

    // If the current OpenGL driver doesn't have framebuffers methods, check if an extension exists
    if (glGenFramebuffers == NULL)
    {
        xLog("OpenGL: glGenFramebuffers is NULL, try to detect an extension\n");

        if (strstr(glExtensions, "ARB_framebuffer_object"))
        {
            xLog("OpenGL: ARB_framebuffer_object is supported\n");
            // The wglGetProcAddress function returns the address of an OpenGL extension function for use with the current OpenGL rendering context
            glIsRenderbuffer = (PFNGLISRENDERBUFFERPROC) wglGetProcAddress("glIsRenderbuffer");
            glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC) wglGetProcAddress("glBindRenderbuffer");
            glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC) wglGetProcAddress("glDeleteRenderbuffers");
            glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC) wglGetProcAddress("glGenRenderbuffers");
            glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC) wglGetProcAddress("glRenderbufferStorage");
            glGetRenderbufferParameteriv = (PFNGLGETRENDERBUFFERPARAMETERIVPROC) wglGetProcAddress("glGetRenderbufferParameteriv");
            glIsFramebuffer = (PFNGLISFRAMEBUFFERPROC) wglGetProcAddress("glIsFramebuffer");
            glBindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC) wglGetProcAddress("glBindFramebuffer");
            glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC) wglGetProcAddress("glDeleteFramebuffers");
            glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC) wglGetProcAddress("glGenFramebuffers");
            glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) wglGetProcAddress("glCheckFramebufferStatus");
            glFramebufferTexture1D = (PFNGLFRAMEBUFFERTEXTURE1DPROC) wglGetProcAddress("glFramebufferTexture1D");
            glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC) wglGetProcAddress("glFramebufferTexture2D");
            glFramebufferTexture3D = (PFNGLFRAMEBUFFERTEXTURE3DPROC) wglGetProcAddress("glFramebufferTexture3D");
            glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC) wglGetProcAddress("glFramebufferRenderbuffer");
            glGetFramebufferAttachmentParameteriv = (PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC) wglGetProcAddress("glGetFramebufferAttachmentParameteriv");
            glGenerateMipmap = (PFNGLGENERATEMIPMAPPROC) wglGetProcAddress("glGenerateMipmap");
        }
        else if (strstr(glExtensions, "EXT_framebuffer_object"))
        {
            xLog("OpenGL: EXT_framebuffer_object is supported\n");
            glIsRenderbuffer = (PFNGLISRENDERBUFFERPROC) wglGetProcAddress("glIsRenderbufferEXT");
            glBindRenderbuffer = (PFNGLBINDRENDERBUFFERPROC) wglGetProcAddress("glBindRenderbufferEXT");
            glDeleteRenderbuffers = (PFNGLDELETERENDERBUFFERSPROC) wglGetProcAddress("glDeleteRenderbuffersEXT");
            glGenRenderbuffers = (PFNGLGENRENDERBUFFERSPROC) wglGetProcAddress("glGenRenderbuffersEXT");
            glRenderbufferStorage = (PFNGLRENDERBUFFERSTORAGEPROC) wglGetProcAddress("glRenderbufferStorageEXT");
            glGetRenderbufferParameteriv = (PFNGLGETRENDERBUFFERPARAMETERIVPROC) wglGetProcAddress("glGetRenderbufferParameterivEXT");
            glIsFramebuffer = (PFNGLISFRAMEBUFFERPROC) wglGetProcAddress("glIsFramebufferEXT");
            glBindFramebuffer = (PFNGLBINDFRAMEBUFFERPROC) wglGetProcAddress("glBindFramebufferEXT");
            glDeleteFramebuffers = (PFNGLDELETEFRAMEBUFFERSPROC) wglGetProcAddress("glDeleteFramebuffersEXT");
            glGenFramebuffers = (PFNGLGENFRAMEBUFFERSPROC) wglGetProcAddress("glGenFramebuffersEXT");
            glCheckFramebufferStatus = (PFNGLCHECKFRAMEBUFFERSTATUSPROC) wglGetProcAddress("glCheckFramebufferStatusEXT");
            glFramebufferTexture1D = (PFNGLFRAMEBUFFERTEXTURE1DPROC) wglGetProcAddress("glFramebufferTexture1DEXT");
            glFramebufferTexture2D = (PFNGLFRAMEBUFFERTEXTURE2DPROC) wglGetProcAddress("glFramebufferTexture2DEXT");
            glFramebufferTexture3D = (PFNGLFRAMEBUFFERTEXTURE3DPROC) wglGetProcAddress("glFramebufferTexture3DEXT");
            glFramebufferRenderbuffer = (PFNGLFRAMEBUFFERRENDERBUFFERPROC) wglGetProcAddress("glFramebufferRenderbufferEXT");
            glGetFramebufferAttachmentParameteriv = (PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC) wglGetProcAddress("glGetFramebufferAttachmentParameterivEXT");
            glGenerateMipmap = (PFNGLGENERATEMIPMAPPROC) wglGetProcAddress("glGenerateMipmapEXT");
        }
        else
        {
            xLog("OpenGL: No framebuffers extension is supported\n");
            xLog("OpenGL: Any call to FBO will crash!\n");
            return false;
        }
    }

    return true;
}

static xEGLView *g_mainWindow = NULL;
static const WCHAR *g_windowClassName = L"X Win32";

static LRESULT CALLBACK _WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    if (g_mainWindow && g_mainWindow->getHWnd() == hWnd)
    {
        return g_mainWindow->WindowProc(uMsg, wParam, lParam);
    }
    else
    {
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
}

xEGLView::xEGLView()
: _hwnd(NULL)
, _hdc(NULL)
, _hrc(NULL)
{
    strcpy(_viewName, "X Win32");
}

xEGLView::~xEGLView()
{

}

bool xEGLView::isOpenGLReady()
{
    return (_hdc != NULL && _hrc != NULL);
}

void xEGLView::end()
{
    if (_hwnd)
    {
        ::DestroyWindow(_hwnd);
        _hwnd = NULL;
    }

    g_mainWindow = NULL;
    UnregisterClass(g_windowClassName, GetModuleHandle(NULL));

    delete this;
}

void xEGLView::swapBuffers()
{
    if (_hdc != NULL)
    {
        ::SwapBuffers(_hdc);
    }
}

#define X_BREAK_IF(cond) if (cond) break
bool xEGLView::create()
{
    bool ret = false;

    do 
    {
        X_BREAK_IF(_hwnd);

        HINSTANCE hInstance = ::GetModuleHandle(NULL);
        WNDCLASS  wc;

        // Redraw on size and own dc for window
        wc.style          = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
        wc.lpfnWndProc    = _WindowProc;                        // WndProc handles messages
        wc.cbClsExtra     = 0;                                  // No extra window data
        wc.cbWndExtra     = 0;                                  // No extra window data
        wc.hInstance      = hInstance;                          // Set the instance
        wc.hIcon          = LoadIcon( NULL, IDI_WINLOGO );      // Load the default icon
        wc.hCursor        = LoadCursor( NULL, IDC_ARROW );      // Load the arrow pointer
        wc.hbrBackground  = NULL;                               // No background required for GL
        wc.lpszMenuName   = NULL;                               // No menu name
        wc.lpszClassName  = g_windowClassName;                  // Set the class name

        X_BREAK_IF(!RegisterClass(&wc) && 1410 != GetLastError());

        // Center window position
        RECT rcDesktop;
        ::GetWindowRect(::GetDesktopWindow(), &rcDesktop);

        WCHAR buf[64] = {0};
        MultiByteToWideChar(CP_UTF8, 0, _viewName, -1, buf, sizeof(buf));

        // Create window
        _hwnd = CreateWindowEx(
            WS_EX_APPWINDOW | WS_EX_WINDOWEDGE,                 // Extended Style For The Window
            g_windowClassName,                                  // Class Name
            buf,                                                // Window Title
            WS_CAPTION | WS_POPUPWINDOW | WS_MINIMIZEBOX,       // Defined Window Style
            0, 0,                                               // Window Position
            //TODO: Initializing width with a large value to avoid getting a wrong client area by 'GetClientRect' function.
            1000,                                               // Window Width
            1000,                                               // Window Height
            NULL,                                               // No Parent Window
            NULL,                                               // No Menu
            hInstance,                                          // Instance
            NULL );

        X_BREAK_IF(!_hwnd);

        ret = initGL();

        if (!ret)
        {
            destroyGL();
        }

        X_BREAK_IF(!ret);
        g_mainWindow = this;
    
    } while (0);

    return ret;
}

bool xEGLView::initGL()
{
    _hdc = ::GetDC(_hwnd);
    setupPixelFormat(_hdc);
    // Create OpenGL rendering context
    _hrc = ::wglCreateContext(_hdc);
    ::wglMakeCurrent(_hdc, _hrc);

    // Check OpenGL version at first
    const GLubyte *glVersion = ::glGetString(GL_VERSION);
    
    if (atof((const char *)glVersion) < 1.5)
    {
        char strComplain[256] = {0};
        sprintf(strComplain,
            "OpenGL 1.5 or higher is required (your version is %s). Please upgrad the driver of your video card.",
            glVersion);
        xMessageBox(strComplain, "OpenGL version too old");

        return false;
    }

    GLenum glewInitResult = glewInit();

    if (GLEW_OK != glewInitResult)
    {
        xMessageBox((char *)glewGetErrorString(glewInitResult), "OpenGL error");
        return false;
    }

    if (GLEW_ARB_vertex_shader && GLEW_ARB_fragment_shader)
    {
        xLog("Ready for GLSL");
    }
    else
    {
        xLog("Not totally ready :(");
    }

    if (glewIsSupported("GL_VERSION_2_0"))
    {
        xLog("Ready for OpenGL 2.0");
    }
    else
    {
        xLog("OpenGL 2.0 not supported");
    }

    if (glewDynamicBinding() == false)
    {
        xMessageBox("No OpenGL framebuffer support. Please upgrad", "OpenGL error");
        return false;
    }

    // Enable point size by default on windows
    glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);

    return true;
}

void xEGLView::destroyGL()
{
    if (_hdc != NULL && _hrc != NULL)
    {
        // Deselect rendering context and delete it
        wglMakeCurrent(_hdc, NULL);
        wglDeleteContext(_hrc);
    }
}

LRESULT xEGLView::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_LBUTTONDOWN:
    	break;

    case WM_PAINT:
        PAINTSTRUCT ps;
        ::BeginPaint(_hwnd, &ps);
        ::EndPaint(_hwnd, &ps);
        break;

    case WM_CLOSE:
        end();
        break;

    case WM_DESTROY:
        destroyGL();
        ::PostQuitMessage(0);
        break;

    default:
        return DefWindowProc(_hwnd, message, wParam, lParam);
    }

    return 0;
}

HWND xEGLView::getHWnd()
{
    return _hwnd;
}

void xEGLView::resize(int width, int height)
{
    if (!_hwnd)
    {
        return;
    }

    RECT rcWindow;
    ::GetWindowRect(_hwnd, &rcWindow);

    RECT rcClient;
    ::GetClientRect(_hwnd, &rcClient);

    // Calculate new window width and height
    POINT ptDiff;
    ptDiff.x = (rcWindow.right - rcWindow.left) - rcClient.right;
    ptDiff.y = (rcWindow.bottom - rcWindow.top) - rcClient.bottom;
    rcClient.right = rcClient.left + width;
    rcClient.bottom = rcClient.top + height;

    const xSize &frameSize = getFrameSize();

    if (frameSize.width > 0)
    {
        WCHAR wszBuf[MAX_PATH] = {0};
#ifdef _DEBUG
        char szBuf[MAX_PATH + 1];
        memset(szBuf, 0, sizeof(szBuf));
        _snprintf(szBuf, MAX_PATH, "%s - %0.0fx%0.0f - %0.2f",
            _viewName, frameSize.width, frameSize.height, 1.0f);
        MultiByteToWideChar(CP_UTF8, 0, szBuf, -1, wszBuf, sizeof(wszBuf));
#else
        MultiByteToWideChar(CP_UTF8, 0, _viewName, -1, wszBuf, sizeof(wszBuf));
#endif
        SetWindowText(_hwnd, wszBuf);
    }

    ::AdjustWindowRectEx(&rcClient, ::GetWindowLong(_hwnd, GWL_STYLE), FALSE, ::GetWindowLong(_hwnd, GWL_EXSTYLE));

    // change width and height
    ::SetWindowPos(_hwnd, 0, 0, 0, width + ptDiff.x, height + ptDiff.y, SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOZORDER);
}

void xEGLView::centerWindow()
{
    if (!_hwnd)
    {
        return;
    }

    RECT rcDesktop, rcWindow;
    ::GetWindowRect(::GetDesktopWindow(), &rcDesktop);

    // subtract the task bar
    HWND hTaskBar = ::FindWindow(TEXT("Shell_TrayWnd"), NULL);
    if (hTaskBar != NULL)
    {
        APPBARDATA abd;

        abd.cbSize = sizeof(APPBARDATA);
        abd.hWnd = hTaskBar;

        SHAppBarMessage(ABM_GETTASKBARPOS, &abd);
        SubtractRect(&rcDesktop, &rcDesktop, &abd.rc);
    }

    ::GetWindowRect(_hwnd, &rcWindow);

    int offsetX = (rcDesktop.right - rcDesktop.left - (rcWindow.right - rcWindow.left)) / 2;
    offsetX = (offsetX > 0) ? offsetX : rcDesktop.left;
    int offsetY = (rcDesktop.bottom - rcDesktop.top - (rcWindow.bottom - rcWindow.top)) / 2;
    offsetY = (offsetY > 0) ? offsetY : rcDesktop.top;

    ::SetWindowPos(_hwnd, 0, offsetX, offsetY, 0, 0, SWP_NOCOPYBITS | SWP_NOSIZE | SWP_NOOWNERZORDER | SWP_NOZORDER);
}

xEGLView *xEGLView::sharedOpenGLView()
{
    static xEGLView *s_pEglView = NULL;

    if (s_pEglView == NULL)
    {
        s_pEglView = new xEGLView();
        if(!s_pEglView->create())
        {
            delete s_pEglView;
            s_pEglView = NULL;
        }
    }

    return s_pEglView;
}

const xSize &xEGLView::getFrameSize() const
{
    return _screenSize;
}

void xEGLView::setViewName(const char *viewName)
{
    if (viewName != NULL && strlen(viewName) > 0)
    {
        strncpy(_viewName, viewName, sizeof(_viewName));
    }
}

void xEGLView::setFrameSize(float width, float height)
{
    _screenSize = xSize(width, height);

    resize(width, height);
    centerWindow();
}

// implementation of xSize

xSize::xSize(void)
{
    setSize(0.0f, 0.0f);
}

xSize::xSize(float width, float height)
{
    setSize(width, height);
}

xSize::xSize(const xSize &other)
{
    setSize(other.width, other.height);
}

xSize& xSize::operator= (const xSize &other)
{
    setSize(other.width, other.height);
    return *this;
}

void xSize::setSize(float width, float height)
{
    this->width = width;
    this->height = height;
}

bool xSize::equals(const xSize &target) const
{
    return ((width == target.width) && (height == target.height));
}
